from os import replace
from LogManager import log_output
import sqlite3
from UserManager import UserManager
from BookManager import BookManager
from BPR.Recommender import Recommender
import time
from typing import Tuple, Dict, List
import numpy as np
import torch
import requests
import json


# Recommend Database
DATABASE = 'database/recommend_data.db'


class RecommendManager:
    def __init__(self, user_manager: UserManager, book_manager: BookManager, recommender: Recommender):
        self.user_manager = user_manager
        self.book_manager = book_manager
        self.recommender = recommender
        self.show_info = self.recommender.data_loader.show_info

        self.error_info = {
            'ok': False
        }
        self.database = DATABASE
        self.ad_books = None
        self.generate_hot_recommendation()

    def get_conn(self):
        self.conn = sqlite3.connect(self.database)
        return self.conn

    def close_conn(self):
        if self.conn is None:
            return

        try:
            self.conn.close()
        except Exception as err:
            log_output(str(err))

        self.conn = None

    def generate_hot_recommendation(self, max_cnt=50):
        hot_book_name_set = set()
        hot_books = []
        hot_i = self.recommender.hot_book_i
        cnt = 0

        for book_id in hot_i:
            book_info = self.book_manager.get_book_info(book_id)
            if book_info.title in hot_book_name_set:
                continue
            hot_book_name_set.add(book_info.title)
            hot_books.append({
                'book_id': book_id,
                'title': book_info.title,
                'author': book_info.author,
                'shortIntro': book_info.shortintro,
                'cate': book_info.cate,
                'cover': book_info.cover,
                'label': '热门'
            })
            cnt += 1
            if cnt >= max_cnt:
                break

        self.hot_books = hot_books

    def get_recent_recommendation(self, user_id: int) -> List[int]:
        """
        Get recent recommend book_id.
        recent_recommend(recent_recommend_id, user_id, book_id)
        """
        try:
            sql = f'SELECT book_id FROM recent_recommend WHERE user_id = {user_id}'
            conn = self.get_conn()
            cursor = conn.cursor()
            rows = cursor.execute(sql)

            result = []
            for row in rows:
                result.append(row[0])

            return result
        except Exception as err:
            log_output(str(err))
            raise ValueError()
        finally:
            if cursor is not None:
                cursor.close()
            self.close_conn()

    def delete_recent_recommendation(self, user_id: int):
        """
        Delete recent recommend for user_id.
        recent_recommend(recent_recommend_id, user_id, book_id)
        """
        try:
            sql = f'DELETE FROM recent_recommend WHERE user_id = {user_id}'
            conn = self.get_conn()
            cursor = conn.cursor()
            cursor.execute(sql)

            conn.commit()
        except Exception as err:
            log_output(str(err))
            raise ValueError()
        finally:
            if cursor is not None:
                cursor.close()
            self.close_conn()

    def add_to_recent_recommendation(self, user_id: int, recent_recommendation: List[int]):
        """
        Add to recent recommend for user_id.
        recent_recommend(recent_recommend_id, user_id, book_id)
        """
        try:
            conn = self.get_conn()
            cursor = conn.cursor()

            for book_id in recent_recommendation:
                sql = f'INSERT INTO recent_recommend VALUES (NULL, {user_id}, {book_id})'
                cursor.execute(sql)

            conn.commit()
        except Exception as err:
            log_output(str(err))
            raise ValueError()
        finally:
            if cursor is not None:
                cursor.close()
            self.close_conn()

    def generate_recommendation(self, user_id: int, max_cnt: int = 25, ax=None, ay=None, az=None):
        # TODO: ax, ay, az, time -> extra_info
        if ax is not None and ay is not None and az is not None:
            a_max = 10
            a_min = -10
            ax = (ax if (ax < a_max) else a_max)
            ay = (ay if (ay < a_max) else a_max)
            az = (az if (az < a_max) else a_max)
            ax = (ax if (ax > a_min) else a_min)
            ay = (ay if (ay > a_min) else a_min)
            az = (az if (az > a_min) else a_min)
            ax /= 10
            ay /= 10
            az /= 10  # normalize ax,ay,az

            localtime = time.localtime(time.time())
            t = (localtime[3]*3600 + localtime[4]
                 * 60 + localtime[5])/(12*3600) - 1
            # normalize time based on seconds,00:00->-1 12:00->0 24:00->1

            data = [ax, ay, az, t]

            extra_info = torch.tensor(data)
        else:
            extra_info = None
            log_output('WARNING: extra info is not used')
        p_result, i_result = self.recommender.get_recommend_p(
            user_id, extra_info=extra_info)

        if p_result is None or i_result is None:
            log_output(
                f'No recommendation for user {user_id}. Fall back to hot recommendation')
            p_result = self.recommender.hot_book_p
            i_result = self.recommender.hot_book_i

        # Sample recommendations
        recent_recommendation = self.get_recent_recommendation(user_id)
        recommend_result = []

        p_sum = np.sum(p_result)
        p_result = [i/p_sum for i in p_result]

        while len(recommend_result) < max_cnt:
            nxt_recommend = np.random.choice(i_result, 1, p=p_result)[0]
            if nxt_recommend not in recent_recommendation and nxt_recommend not in recommend_result:
                recommend_result.append(int(nxt_recommend))

        self.delete_recent_recommendation(user_id)
        self.add_to_recent_recommendation(user_id, recommend_result)

        return recommend_result

    def insert_ad(self, user_id: int, books: List[Dict[str, str]]) -> Dict[str, str]:
        if self.ad_books is None:
            # Generate ad books
            book_cnt = self.recommender.file_loader.book_cnt
            ad_book_id = np.random.choice(range(book_cnt), 20, replace=False)
            self.ad_books = []
            for book_id in ad_book_id:
                book_info = self.book_manager.get_book_info(book_id)
                self.ad_books.append({
                    'book_id': book_info.book_id,
                    'title': book_info.title,
                    'author': book_info.author,
                    'shortIntro': book_info.shortintro,
                    'cate': book_info.cate,
                    'cover': book_info.cover,
                    'label': '广告'
                })

        cur_hot_books = np.random.choice(self.hot_books, 8, replace=False)

        sorted_ad_id = self.recommender.get_sorted_recommend_list(
            user_id, [i['book_id'] for i in self.ad_books])[:6]
        personalized_ad_books = [self.ad_books[i] for i in sorted_ad_id]

        insert_gap = len(books) // len(personalized_ad_books)
        ad_pos = 2

        for book in personalized_ad_books:
            books.insert(ad_pos, book)
            ad_pos = min(len(books)-1, ad_pos+insert_gap)

        for book in cur_hot_books:
            index = np.random.randint(0, len(books))
            books.insert(index, book)

        return books

    def get_recommendation(self, send_data: Dict[str, str]) -> Dict[str, str]:
        if type(send_data) != dict:
            log_output('Send Interaction not Dict', print_to_screen=True)
            return self.error_info

        assert 'token' in send_data

        ax, ay, az = None, None, None
        if 'ax' in send_data and 'ay' in send_data and 'az' in send_data:
            ax = send_data['ax']
            ay = send_data['ay']
            az = send_data['az']

        user_id = self.user_manager.get_user_id_with_token(send_data['token'])
        recommend_result = self.generate_recommendation(
            user_id, ax=ax, ay=ay, az=az)

        books = []
        for book_id in recommend_result:
            book_info = self.book_manager.get_book_info(book_id)
            books.append({
                'book_id': book_id,
                'title': book_info.title,
                'author': book_info.author,
                'shortIntro': book_info.shortintro,
                'cate': book_info.cate,
                'cover': book_info.cover,
                'label': '推荐'
            })

        return_dict = {
            'ok': True,
            'total': len(books),
            'books': books
        }

        return return_dict

    def request_hot_book(self) -> Dict[str, str]:
        return_dict = {
            'ok': True,
            'total': len(self.hot_books),
            'books': self.hot_books
        }

        return return_dict
